修复 Form 组件 schema 循环更新的问题
问题描述
Form 组件中存在 schema 变更导致 model 循环更新的 bug。当 schema 变化时,会触发 watch 回调更新 internal model,而 internal model 的变更又会反向触发 schema 相关的 watch,形成无限循环。
问题定位
循环触发链路
schema 变更
→ watch(schema) 触发
→ 更新 internalModel
→ watch(internalModel) 触发
→ 又更新 schema 相关数据
→ 循环...
text
根因
在 useForm.ts 中,schema 和 model 之间的 watch 没有加防循环标志位,导致双向绑定形成闭环。
修复方案
引入 isUpdatingInternalModel 标志位
// composables/useForm.ts
import { ref, reactive, watch, onBeforeMount, type Ref } from 'vue'
import type { FormSchema } from '@/components/form/types'
interface UseFormOptions {
schema: Ref<FormSchema[]>
modelValue?: Record<string, any>
}
export function useForm(options: UseFormOptions) {
const { schema, modelValue } = options
// 防循环标志位
const isUpdatingInternalModel = ref(false)
// 内部响应式对象
const internalModel = reactive<Record<string, any>>({})
// 行配置
const rows = ref<any[]>([])
/**
* 根据 schema 初始化 internalModel
*/
function setForm(schemaItems: FormSchema[]) {
schemaItems.forEach((item) => {
if (!(item.prop in internalModel)) {
internalModel[item.prop] = item.value
}
})
}
// 初始化
onBeforeMount(() => {
setForm(schema.value)
})
// 监听 schema 变更 → 更新 internalModel(单向)
watch(
() => schema.value,
(newSchema) => {
isUpdatingInternalModel.value = true
setForm(newSchema)
// nextTick 后重置标志位
nextTick(() => {
isUpdatingInternalModel.value = false
})
},
{ deep: true }
)
// 监听 internalModel 变更 → 通知外层(跳过内部更新)
watch(
() => ({ ...internalModel }),
(newModel) => {
if (isUpdatingInternalModel.value) return // 关键:跳过内部触发的更新
// emit('update:modelValue', newModel)
},
{ deep: true }
)
return {
internalModel,
rows,
setForm,
isUpdatingInternalModel
}
}
typescript
修复前后对比
// 修复前:无防循环,schema 变更 → model 变更 → 又触发 schema watch
watch(() => schema.value, (newVal) => {
setForm(newVal) // 直接触发 model 更新,无保护
})
watch(() => internalModel, (newVal) => {
// 每次 model 变更都通知外层,包括 schema 触发的
emit('update:modelValue', newVal)
})
// 修复后:isUpdatingInternalModel 阻断循环
watch(() => schema.value, (newVal) => {
isUpdatingInternalModel.value = true // 设置标志
setForm(newVal)
nextTick(() => {
isUpdatingInternalModel.value = false // 重置标志
})
})
watch(() => internalModel, (newVal) => {
if (isUpdatingInternalModel.value) return // 跳过内部触发的更新
emit('update:modelValue', newVal)
})
typescript
修复步骤总结
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 删除旧的 watch 逻辑 | 移除 Form 组件中旧的 watch 和 setForm |
| 2 | 添加标志位 | isUpdatingInternalModel = ref(false) |
| 3 | 修改 schema watch | 更新 model 前设置标志位,nextTick 后重置 |
| 4 | 修改 model watch | 检查标志位,内部更新时跳过通知 |
| 5 | 验证测试 | 修改 schema、修改 model 均不再循环 |
常见循环更新场景
| 场景 | 原因 | 解决方案 |
|---|---|---|
| schema → model → schema | 双向 watch 无保护 | isUpdating 标志位 |
| 外层 v-model → 内层 model → 外层 v-model | emit 触发外层更新 | isInternalUpdate flag |
| deep watch 嵌套对象 | 深层属性变更多次触发 | shallowRef 或手动 triggerRef |
实践要点
- 使用
isUpdatingInternalModel标志位打断 watch 循环链路 - 在
nextTick回调中重置标志位,确保时序正确 - 修复后 schema 变更只允许一次性更新 model,不反向传播
- 外层 model 变更也只允许一次性同步到内层,避免双向循环
- 所有双向绑定的 watch 都应考虑循环更新问题,养成加标志位的习惯
↑